What are we doing?

Data sitting on a computer somewhere is pretty dull. If you are working with data, it's a good idea to find lots of ways to interact with it. See plots of the inputs and outputs of your models, plot statistics of it etc. If you work with data specific to your field, there'll likely be lots of ways you can think of to interact with it.

For example if it's images, look at them. If you transform your data for any reason, look at them before and after the transformation. It sounds obvious but I beleive it can be overlooked by machine learning engineers / data scientists because building tools or bespoke visualisations to interact with data can sometimes feels out of the scope of their responsibilities.

Ok, preaching aside, let's create something that will help people who work with audio within Jupyter notebooks to interact with it. First things first we want to be able to hear it.

The end goal is to have a bidirectional interactive audio plot for interacting with audio visualisation plots like this tweet.

Conveniently, IPython comes with lots of out-of-the-box ways to display data. Here's one for audio:

from IPython import display
audio_path = "./my_icons/blah.wav"
display.Audio(filename=audio_path)

Although this lets us hear the audio, what if we want to see it? Let's first look at what's inside it:

from scipy.io import wavfile
sr, wav_data = wavfile.read(audio_path)
print(sr)
print(wav_data.shape)
48000
(775922, 2)

This shows the sample rate is 48000Hz and it has 775922 samples for 2 channels.

wav_data[:,0]
array([-2, -3,  0, ..., -3, -1,  0], dtype=int16)

Seeing audio in a big numpy array isn't very useful. But what if we plot the values:

%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(wav_data)
plt.show()

The two channels are on top of eachother. We can split them like so:

fig, axs = plt.subplots(2)
axs[0].plot(wav_data[:,0])
axs[1].plot(wav_data[:,1])
plt.show()
display.Audio(audio_path)

Although this is nice, I'd like to have the x-axis be seconds rather than samples. I'm going to use the badly named (in my opinion) numpy.linspace to do this. It just gives use evenly spaced numbers between start and end, and we can decide how many numbers.

The duration is just the number of samples divided by the sample rate, and we want the same number of points (to match our y axis).

import numpy as np
fig, axs = plt.subplots(2)
duration = len(wav_data)/sr
x = np.linspace(0, duration, len(wav_data))
axs[0].plot(x, wav_data[:,0])
axs[1].plot(x, wav_data[:,1])
plt.show()
display.Audio(audio_path)

Ok, that's better but is there any better way to view audio than using the amplitude of the waveform??

Smarter people than me came up with viewing audio frequencies rather than amplitudes. 'Spectrograms' of audio are used to display this. They are visualisations of the frequency changes over time.

plt.specgram(wav_data[:,0], NFFT=1024, Fs=44100, noverlap=900)
plt.show()
display.Audio(audio_path)

We can do the same thing using scipy to first get the spectogram and then use matplotlib to plot it with a colormesh

from scipy.signal import spectrogram
f, t, sxx = spectrogram(wav_data[:,0], sr)
plt.pcolormesh(t, f, 10*np.log10(sxx))
plt.show()
display.Audio(audio_path)

Scrub

That's getting us close to what we want, but what we really want is to be able to interact with the plot and hear the audio at the point we interact with.

For more interactivity, we're going to reach for a different tool other than matplotlib and IPython.display. Holoviews with Panel by Anaconda are very nice for custom interactivity. Conveniently for us, Holoviews Image component and Panel's Audio component are a more powerful than the default IPython one and they play nicely together.

import holoviews as hv 
import panel as pn
hv.extension("bokeh")

spec_gram = hv.Image((t, f, np.log10(sxx)), ["Time (s)", "Frequency (hz)"]).opts(width=600)
audio = pn.pane.Audio(wav_data[:,0], sample_rate=sr, name='Audio', throttle=500)
pn.Column(audio,
          spec_gram)
</img> </img>
from bokeh.resources import INLINE

pn.Column(audio,
          spec_gram).save('audio', embed=True, resources=INLINE)

We want the playhead to update when the time changes while you're playing it. To do this, we'll be using DynamicMap, the Stream from audio.param.time and a callback which returns a Vline (the playhead).

def update_playhead(time):
    return hv.VLine(time).opts(color='green')

dmap_time = hv.DynamicMap(update_playhead, streams=[audio.param.time]).opts(width=600)
# pn.Column(audio,
#          spec_gram*dmap_time)
# TODO: Add gif here

That works great, but we also want to be able to click the plot and update the playhead. We do this by merging the two streams to trigger one update_playhead callback. SingleTap captures when the plot is clicked, and we use Params to update time to t for the merged callback.

from scipy.signal import spectrogram
import holoviews as hv 
import panel as pn
hv.extension("bokeh")

f, t, sxx = spectrogram(wav_data[:,0], sr)
spec_gram = hv.Image((t, f, np.log10(sxx)), ["Time (s)", "Frequency (hz)"]).opts(width=600)
audio = pn.pane.Audio(wav_data[:,0], sample_rate=sr, name='Audio', throttle=500)

def update_playhead(x,y,t):
    if x is None:
        return hv.VLine(t).opts(color='green')
    else:
        audio.time = x
        return hv.VLine(x).opts(color='green')

tap_stream = hv.streams.SingleTap(transient=True)
time_play_stream = hv.streams.Params(parameters=[audio.param.time], rename={'time': 't'})
dmap_time = hv.DynamicMap(update_playhead, streams=[time_play_stream, tap_stream])
pn.Column( audio, 
          (spec_gram * dmap_time))
</img> </img>
pn.Column( audio, 
          (spec_gram * dmap_time)).save('audiotest', embed=True, resources=INLINE, max_states=10000)
pause = pn.widgets.Toggle(name='Pause')
pause.jslink(audio, value='paused')
pause
markdown = pn.pane.Markdown("<b>Markdown display</b>", width=400)
markdown = pn.pane.Markdown("<b>Markdown display</b>", width=400)
text_input = pn.widgets.TextInput(value="Markdown display")

code = '''
    target.text = '<b>' + source.value + '</b>'
'''
link = text_input.jslink(markdown, code={'value': code})

pn.Row(text_input, markdown)
code = '''
    target.text = source.value.toString()
'''

link = audio.jslink(markdown, code={'value': code})
pn.Row(audio, markdown)
audio.jscallback(value="""
if (result.value!="None") {
      result.text = "hello"
}
""", args={'result': markdown})
markdown
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-63-8b796b2cb3c6> in <module>
----> 1 markdown.text

AttributeError: 'Markdown' object has no attribute 'text'
audio.jslink(markdown, value='time')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-64-b6299d267784> in <module>
----> 1 audio.jslink(markdown, value='time')

/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py in jslink(self, target, code, args, bidirectional, **links)
   1104                     raise ValueError("Could not jslink %r parameter (or property) "
   1105                                      "on %s object because it was not found.%s"
-> 1106                                     % (p, type(self).__name__, matches))
   1107                 elif (target._source_transforms.get(p, False) is None or
   1108                       target._rename.get(p, False) is None):

ValueError: Could not jslink 'time' parameter (or property) on Audio object because it was not found.
audio.controls(jslink=True)
Traceback (most recent call last):
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/pyviz_comms/__init__.py", line 315, in _handle_msg
    self._on_msg(msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 242, in _on_msg
    doc.unhold()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 669, in unhold
    self._trigger_on_change(event)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1132, in _trigger_on_change
    self._with_self_as_curdoc(event.callback_invoker)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1150, in _with_self_as_curdoc
    return f()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 155, in invoke
    callback(attr, old, new)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 494, in _comm_change
    self._process_events({attr: new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 511, in _process_events
    self.param.set_param(**self._process_property_change(events))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/param.py", line 376, in link_widget
    self.object.param.set_param(**{p_name: change.new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 789, in param_change
    self._update_model(events, msg, root, model, doc, comm)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 767, in _update_model
    model.update(**msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 370, in update
    setattr(self, k, v)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 274, in __setattr__
    super().__setattr__(name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 539, in __set__
    self._internal_set(obj, value, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 760, in _internal_set
    value = self.property.prepare_value(obj, self.name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 331, in prepare_value
    raise e
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 324, in prepare_value
    self.validate(value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 456, in validate
    raise ValueError(msg)
ValueError: expected a value of type Integral, got 99.6 of type float
Traceback (most recent call last):
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/pyviz_comms/__init__.py", line 315, in _handle_msg
    self._on_msg(msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 241, in _on_msg
    patch.apply_to_document(doc, comm.id)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in apply_to_document
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1150, in _with_self_as_curdoc
    return f()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in <lambda>
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 411, in apply_json_patch
    patched_obj.set_from_json(attr, value, models=references, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 341, in set_from_json
    descriptor.set_from_json(self, json, models, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 612, in set_from_json
    models, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 321, in set_from_json
    self._internal_set(obj, json, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 763, in _internal_set
    self._real_set(obj, old, value, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 832, in _real_set
    self._trigger(obj, old, value, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 909, in _trigger
    obj.trigger(self.name, old, value, hint, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/model.py", line 661, in trigger
    super().trigger(attr, old, new, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 157, in trigger
    self._document._notify_change(self, attr, old, new, hint, setter, invoke)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1042, in _notify_change
    self._trigger_on_change(event)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1132, in _trigger_on_change
    self._with_self_as_curdoc(event.callback_invoker)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1150, in _with_self_as_curdoc
    return f()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 155, in invoke
    callback(attr, old, new)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 494, in _comm_change
    self._process_events({attr: new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 511, in _process_events
    self.param.set_param(**self._process_property_change(events))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/param.py", line 376, in link_widget
    self.object.param.set_param(**{p_name: change.new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 789, in param_change
    self._update_model(events, msg, root, model, doc, comm)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 767, in _update_model
    model.update(**msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 370, in update
    setattr(self, k, v)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 274, in __setattr__
    super().__setattr__(name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 539, in __set__
    self._internal_set(obj, value, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 760, in _internal_set
    value = self.property.prepare_value(obj, self.name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 331, in prepare_value
    raise e
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 324, in prepare_value
    self.validate(value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 456, in validate
    raise ValueError(msg)
ValueError: expected a value of type Integral, got 68.3 of type float
Traceback (most recent call last):
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/pyviz_comms/__init__.py", line 315, in _handle_msg
    self._on_msg(msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 241, in _on_msg
    patch.apply_to_document(doc, comm.id)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in apply_to_document
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1150, in _with_self_as_curdoc
    return f()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in <lambda>
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 411, in apply_json_patch
    patched_obj.set_from_json(attr, value, models=references, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 341, in set_from_json
    descriptor.set_from_json(self, json, models, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 612, in set_from_json
    models, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 321, in set_from_json
    self._internal_set(obj, json, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 763, in _internal_set
    self._real_set(obj, old, value, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 832, in _real_set
    self._trigger(obj, old, value, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 909, in _trigger
    obj.trigger(self.name, old, value, hint, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/model.py", line 661, in trigger
    super().trigger(attr, old, new, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 157, in trigger
    self._document._notify_change(self, attr, old, new, hint, setter, invoke)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1042, in _notify_change
    self._trigger_on_change(event)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1132, in _trigger_on_change
    self._with_self_as_curdoc(event.callback_invoker)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1150, in _with_self_as_curdoc
    return f()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 155, in invoke
    callback(attr, old, new)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 494, in _comm_change
    self._process_events({attr: new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 511, in _process_events
    self.param.set_param(**self._process_property_change(events))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/param.py", line 376, in link_widget
    self.object.param.set_param(**{p_name: change.new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 789, in param_change
    self._update_model(events, msg, root, model, doc, comm)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 767, in _update_model
    model.update(**msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 370, in update
    setattr(self, k, v)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 274, in __setattr__
    super().__setattr__(name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 539, in __set__
    self._internal_set(obj, value, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 760, in _internal_set
    value = self.property.prepare_value(obj, self.name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 331, in prepare_value
    raise e
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 324, in prepare_value
    self.validate(value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 456, in validate
    raise ValueError(msg)
ValueError: expected a value of type Integral, got 65.5 of type float
Traceback (most recent call last):
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/pyviz_comms/__init__.py", line 315, in _handle_msg
    self._on_msg(msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 241, in _on_msg
    patch.apply_to_document(doc, comm.id)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in apply_to_document
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1150, in _with_self_as_curdoc
    return f()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in <lambda>
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 411, in apply_json_patch
    patched_obj.set_from_json(attr, value, models=references, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 341, in set_from_json
    descriptor.set_from_json(self, json, models, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 612, in set_from_json
    models, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 321, in set_from_json
    self._internal_set(obj, json, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 763, in _internal_set
    self._real_set(obj, old, value, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 832, in _real_set
    self._trigger(obj, old, value, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 909, in _trigger
    obj.trigger(self.name, old, value, hint, setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/model.py", line 661, in trigger
    super().trigger(attr, old, new, hint=hint, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 157, in trigger
    self._document._notify_change(self, attr, old, new, hint, setter, invoke)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1042, in _notify_change
    self._trigger_on_change(event)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1132, in _trigger_on_change
    self._with_self_as_curdoc(event.callback_invoker)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/document/document.py", line 1150, in _with_self_as_curdoc
    return f()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 155, in invoke
    callback(attr, old, new)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 494, in _comm_change
    self._process_events({attr: new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 511, in _process_events
    self.param.set_param(**self._process_property_change(events))
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/param.py", line 376, in link_widget
    self.object.param.set_param(**{p_name: change.new})
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1365, in set_param
    self_._batch_call_watchers()
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/param/parameterized.py", line 1480, in _batch_call_watchers
    watcher.fn(*events)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 789, in param_change
    self._update_model(events, msg, root, model, doc, comm)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/panel/viewable.py", line 767, in _update_model
    model.update(**msg)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 370, in update
    setattr(self, k, v)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/has_props.py", line 274, in __setattr__
    super().__setattr__(name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 539, in __set__
    self._internal_set(obj, value, setter=setter)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 760, in _internal_set
    value = self.property.prepare_value(obj, self.name, value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 331, in prepare_value
    raise e
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 324, in prepare_value
    self.validate(value)
  File "/opt/anaconda3/envs/laptop-env/lib/python3.7/site-packages/bokeh/core/property/bases.py", line 456, in validate
    raise ValueError(msg)
ValueError: expected a value of type Integral, got 65.10000000000001 of type float
value1 =   pn.widgets.Spinner(value=0, width=75)
operator = pn.widgets.Select(value='*', options=['*', '+'], width=50, align='center')
value2 =   pn.widgets.Spinner(value=0, width=75)
button =   pn.widgets.Button(name='=', width=50)
result =   pn.widgets.StaticText(value='0', width=50, align='center')

button.jscallback(clicks="""
if (op.value == '*') 
  result.text = (v1.value * v2.value).toString()
else
  result.text = (v1.value + v2.value).toString()
""", args={'op': operator, 'result': result, 'v1': value1, 'v2': value2})

pn.Row(value1, operator, value2, button, result)

Play with it yourself!

Here it is deployed on PyViz examples: https://genetic-algorithm.pyviz.demo.anaconda.com/GA.

You can also view and run all the code yourself from here. Thanks for reading.

I personally love learning about these kind of algorithms and finding ways to interact with them visually. What do you think about these nature-inspired algorithms? Did you learn a bit about creating interactive visualisations in Python by reading this article? If so, feel free to share it, and you’re also more than welcome to contact me (via Twitter) if you have any questions, comments, or feedback.

Thanks for reading! :rocket:

Follow me on Twitter here for more stuff like this.